﻿'Copyright (c) 2009 Grailtek

'Permission is hereby granted, free of charge, to any person
'obtaining a copy of this software and associated documentation
'files (the "Software"), to deal in the Software without
'restriction, including without limitation the rights to use,
'copy, modify, merge, publish, distribute, sublicense, and/or sell
'copies of the Software, and to permit persons to whom the
'Software is furnished to do so, subject to the following
'conditions:

'The above copyright notice and this permission notice shall be
'included in all copies or substantial portions of the Software.

'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
'EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
'OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
'NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
'HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
'WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
'FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
'OTHER DEALINGS IN THE SOFTWARE.

Imports System.Reflection
Imports System.Data.Common
Imports System.ExecutorServices
Imports System.DataExecutorServices.Exceptions
Imports System.Aspect

<AttributeUsage(AttributeTargets.Delegate)> _
Public MustInherit Class DataCallBaseAttribute
    Inherits ContractVerbAttribute
    Implements IDataCall

    Public Sub New(ByVal iMethodUIName As String, ByVal iStoredProcedureName As String, ByVal iDatabase As String, ByVal iConnectionString As String)
        MyBase.New(iStoredProcedureName, iMethodUIName)
        _Database = iDatabase
        _ConnectionString = iConnectionString
    End Sub

    Protected MustOverride Sub Execute(ByVal args() As Argument) Implements IAspectAction.Execute
    Protected MustOverride Function ExecuteReturnTable(ByVal args() As Argument) As DataTable

    Private ReadOnly _Database As String
    Public ReadOnly Property Database() As String Implements IDataCall.Database
        Get
            Return _Database
        End Get
    End Property
    Private ReadOnly _ConnectionString As String
    Public ReadOnly Property ConnectionString() As String Implements IDataCall.ConnectionString
        Get
            Return _ConnectionString
        End Get
    End Property
    Public Shared ReadOnly AspectID As New Guid("EDEB9DD5-0594-4f01-89B4-079CFD0DE5E6")
    Private ReadOnly Property _AspectID() As System.Guid Implements IAspectAction.AspectID
        Get
            Return AspectID
        End Get
    End Property
    Public Shared ReadOnly _AspectUIName As String = "data call"
    Public Overridable ReadOnly Property AspectUIName() As String Implements IAspect.AspectUIName
        Get
            Return _AspectUIName
        End Get
    End Property

    Protected Function GetItem(Of T As New)(ByVal Outputs() As OutputMapping, ByVal args() As Argument) As T Implements IAspectFunction.GetItem
        Dim ret As T
        If GetType(DataTable).IsAssignableFrom(GetType(T)) Then
            ret = DirectCast(DirectCast(ExecuteReturnTable(args), Object), T)
        ElseIf GetType(T) Is GetType(Guid) Then
            Using dt As DataTable = ExecuteReturnTable(args)
                If Not dt Is Nothing AndAlso dt.Rows.Count > 0 AndAlso dt.Columns.Count > 0 Then
                    Dim o As Object = dt.Rows(0)(0)
                    If Not IsDBNull(o) Then
                        ret = DirectCast(o, T)
                    End If
                End If
            End Using
        Else
            Using dt As DataTable = ExecuteReturnTable(args)
                If Not dt Is Nothing AndAlso dt.Columns.Count > 0 AndAlso dt.Rows.Count > 0 Then
                    Dim kvps As List(Of KeyValuePair(Of DataColumn, OutputMapping)) = GetColumnMapping(Outputs, dt)
                    ret = New T
                    For Each kvp As KeyValuePair(Of DataColumn, OutputMapping) In kvps
                        PopulateField(ret, dt.Rows(0), kvp)
                    Next
                End If
            End Using
        End If
        Return ret
    End Function

    Private Function GetColumnMapping(ByVal Outputs() As OutputMapping, ByVal dt As DataTable) As List(Of KeyValuePair(Of DataColumn, OutputMapping))
        Dim ret As New List(Of KeyValuePair(Of DataColumn, OutputMapping))
        For Each op As OutputMapping In Outputs
            If Not dt.Columns.Contains(op.FieldAttribute.RegisterName) Then
                Dim msg As String = String.Format("Expected field ""{0}"" was missing from the result of calling method ""{1}"".", op.FieldAttribute.RegisterName, Me.RegisterName)
                Throw New DbTypeMappingException(msg)
            Else
                ret.Add(New KeyValuePair(Of DataColumn, OutputMapping)(dt.Columns(op.FieldAttribute.RegisterName), op))
            End If
        Next
        Return ret
    End Function

    Private Shared Sub PopulateField(Of T)(ByVal item As T, ByVal row As DataRow, ByVal kvp As KeyValuePair(Of DataColumn, OutputMapping))
        Dim o As Object = row(kvp.Key)
        If Not IsDBNull(o) Then
            'TODO: consolidated casting logic with proper common-sense conversions and casting checks, possible shift to IL emit (esp! if we move to interfaces)
            kvp.Value.SetValue(item, o)
        End If
    End Sub

    Protected Function GetItems(Of T As New)(ByVal Outputs() As OutputMapping, ByVal args() As Argument) As T() Implements IAspectFunction.GetItems
        Dim ret(-1) As T
        Using dt As DataTable = ExecuteReturnTable(args)
            If Not dt Is Nothing AndAlso dt.Columns.Count > 0 Then
                Dim rowUpperBound As Integer = dt.Rows.Count - 1
                ReDim ret(rowUpperBound)
                Dim kvps As List(Of KeyValuePair(Of DataColumn, OutputMapping)) = GetColumnMapping(Outputs, dt)
                For i As Integer = 0 To rowUpperBound
                    Dim n As New T
                    ret(i) = n
                    For Each kvp As KeyValuePair(Of DataColumn, OutputMapping) In kvps
                        PopulateField(n, dt.Rows(i), kvp)
                    Next
                Next
            End If
        End Using
        Return ret
    End Function

    Protected Function GetValue(Of T As IConvertible)(ByVal args() As Argument) As T Implements IAspectFunction.GetValue
        Dim ret As T = Nothing
        Using dt As DataTable = ExecuteReturnTable(args)
            If Not dt Is Nothing AndAlso dt.Rows.Count > 0 AndAlso dt.Columns.Count > 0 Then
                Dim o As Object = dt.Rows(0)(0)
                If Not IsDBNull(o) Then
                    ret = DirectCast(o, T)
                End If
            End If
        End Using
        Return ret
    End Function

    Protected Function GetValues(Of T As System.IConvertible)(ByVal args() As Argument) As T() Implements IAspectFunction.GetValues
        Dim ret(-1) As T
        Using dt As DataTable = ExecuteReturnTable(args)
            If Not dt Is Nothing AndAlso dt.Rows.Count > 0 AndAlso dt.Columns.Count > 0 Then
                Dim firstItem As Object = dt.Rows(0)(0)
                If Not firstItem Is Nothing AndAlso firstItem.GetType.IsArray AndAlso firstItem.GetType.GetElementType Is GetType(T) Then 'we are getting an array directly as a data type from the first row
                    'TODO: support return types of T(,) - though I think it may be bad to use such a thing in practice
                    ret = DirectCast(firstItem, T())
                Else
                    Dim rowUpperBound As Integer = dt.Rows.Count - 1
                    ReDim ret(rowUpperBound)
                    For i As Integer = 0 To rowUpperBound
                        Dim o As Object = dt.Rows(i)(0)
                        If Not IsDBNull(o) Then
                            ret(i) = DirectCast(o, T)
                        End If
                    Next
                End If
            End If
        End Using
        Return ret
    End Function

    Private _DelegateInfo As DelegateInfo
    Public Property DelegateInfo() As DelegateInfo Implements IAspect.DelegateInfo
        Get
            Return _DelegateInfo
        End Get
        Set(ByVal value As DelegateInfo)
            _DelegateInfo = value
        End Set
    End Property

    Public Sub BeforeInvoke(ByVal args() As Aspect.Argument) Implements Aspect.IAspectFunction.BeforeInvoke
        If EmitSQLTrace Then EmitSqlTraceFor(args)
    End Sub

    Public Shared EmitSQLTrace As Boolean = True

    Public Function GetSqlTraceFor(ByVal args() As System.Aspect.Argument) As String
        Dim ret As New Text.StringBuilder
        ret.Append("EXEC [" & Database & "].dbo.[" & RegisterName & "]")
        Dim first As Boolean = True
        For Each arg As Argument In args
            If Not arg.ParamAttribute Is Nothing Then
                Dim strValue As String = "NULL"
                If Not arg.Value Is Nothing And Not IsDBNull(arg.Value) Then
                    If arg.Param.ParameterType.IsAssignableFrom(GetType(Byte())) Then
                        strValue = "BYTE[]"
                    Else
                        If arg.Value.GetType() Is GetType(Guid) Then
                            strValue = "'" & DirectCast(arg.Value, Guid).ToString & "'"
                        Else
                            strValue = "'" & CStr(arg.Value) & "'"
                        End If
                    End If
                End If
                If first Then first = False Else ret.Append(", ")
                ret.Append(" @" & arg.ParamAttribute.RegisterName & " = " & strValue)
            End If
        Next
        Return ret.ToString
    End Function

    Private Sub EmitSqlTraceFor(ByVal args() As System.Aspect.Argument)
        Trace.WriteLine(GetSqlTraceFor(args))
    End Sub

End Class